/**
 * Copyright 2013 Medium Entertainment, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.playhaven.android.view;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.View;
import android.webkit.*;
import com.playhaven.android.Placement;
import com.playhaven.android.PlayHaven;
import com.playhaven.android.PlayHavenException;
import com.playhaven.android.cache.Cache;
import com.playhaven.android.data.*;
import com.playhaven.android.req.UrlRequest;
import com.playhaven.android.util.JsonUtil;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * HTML Content Unit
 */
public class HTMLView extends WebView implements ChildView<HTMLView> {
    private final String DISPATCH_PREFIX = "ph://";
    private Placement mPlacement;
    private List<String> mImages;
    private ArrayList<Reward> mRewards;
    private ArrayList<Purchase> mPurchases;
    private ArrayList<DataCollectionField> mDataFields;

    // These are bits of Javascript we inject into the window. 
    /** callback template arguments: callbackId, JSON, error */
    private final String CALLBACK_TEMPLATE = "javascript:PlayHaven.nativeAPI.callback(\"%s\", %s, %s)";
    private final String DISPATCH_PROTOCOL_TEMPLATE = "javascript:window.PlayHavenDispatchProtocolVersion=4";
    private final String COLLECT_FORM_DATA = "javascript:$.ajax({dataType: 'jsonp', jsonp: 'dcDataCallback', data: $('form').serialize(), url: 'ph://dcData'});";

    /**
     * These match the host portion of DISPATCH_PREFIX urls requested by the content
     * templates. They indicate different events in the content template that
     * need attention from the SDK.
     */
    public enum Dispatches {
        /**
         * closeButton hides the native emergency close button, and passes
         * notice of whether it was hidden back to the content template
         */
        closeButton,
        /**
         * dismiss triggers the contentDismissed listener
         */
        dismiss,
        /**
         * launch retrieves a URL from the server to be parsed using
         * Intent.ACTION_VIEW
         */
        launch,
        /**
         * loadContext passes the full "context" JSON blob to the
         * content template
         */
        loadContext,
        /**
         * purchase stores the purchase object (which is generated by the
         * content template) as mPurchases, for use with dismiss dispatch
         */
        purchase,
        /**
         * reward stores the reward object (which is generated by the
         * content template) as mRewards, for use with dismiss dispatch
         */
        reward,
        /**
         * subcontent takes a JSON blob generated by the content template
         * and uses that to get data for a new impression, currently a
         * more_games widget that follows a featured ad
         */
        subcontent,
        /**
         * No longer used
         */
        track,
        /**
         * This is one injected to let the Android SDK harvest data from the
         * opt-in data collection form.
         */
        dcData
    }

    /**
     * Makes the subcontent request, replaces the model in the placement, and
     * then reloads the WebView with the new stuff.
     */
    private class SubcontentRequest extends com.playhaven.android.req.SubcontentRequest {
        public SubcontentRequest(String dispatchContext) {
            super(dispatchContext);
        }

        @Override
        protected void handleResponse(String json) {
            mPlacement.setModel(json);
            load(JsonUtil.<String>getPath(mPlacement.getModel(), "$.response.url"));
        }
    }

    private WebChromeClient webChromeClient = new WebChromeClient() {
        @Override
        public boolean onConsoleMessage(ConsoleMessage message) {
            PlayHaven.v("ConsoleMessage: %s", message.message());
            return super.onConsoleMessage(message);
        }
    };

    private WebViewClient webViewClient = new WebViewClient() {
        @Override
        public void onLoadResource(WebView view, String url) {
            if (url.startsWith(DISPATCH_PREFIX)) {
                handleDispatch(url);
            }
        }

		@Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            HTMLView.this.setVisibility(android.view.View.VISIBLE);
        }

        @Override
	    public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
	        // Load images or the content template from the cache.
            if(mImages.contains(url) || url.equals(JsonUtil.<String>getPath(mPlacement.getModel(), "$.response.url")))
            {
	            try {
	            	// TODO: spaces will break Cache, fix encoding before now. 
	            	url = url.replace(" ", "%20"); 
	        		Cache cache = new Cache(getContext());
	        		File file = cache.getFile(new URL(url));
	        		cache.close();
	        		if(file != null && file.exists()){
		            	PlayHaven.v("Loading from cache: %s.", file.getAbsolutePath());
		        		InputStream inputStream = new FileInputStream(file);
		        		return new WebResourceResponse("", "UTF-8", inputStream);
	        		}
	            } catch (Exception e){
	            	PlayHaven.e("Could not load from cache: %s.", url);
	            	PlayHaven.e(e);
	            }
	        }
	        return null;
	    }

        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            if (url.startsWith(DISPATCH_PREFIX)) {
                handleDispatch(url);
                return true;
            } else {
                return super.shouldOverrideUrlLoading(view, url);
            }
        }
    };

    public HTMLView(Context context) {
        super(context);
    }

    public HTMLView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * This switches on the host portion of a request prefixed with
     * DISPATCH_PREFIX in order to handle events from the content templates.
     *
     * @TODO this would be a good candidate for factoring out to a cleaner custom WebViewClient
     * 
     * @param dispatchUrl
     */
    private void handleDispatch(String dispatchUrl) {
        Uri callbackUri = Uri.parse(dispatchUrl);
        String callbackId = callbackUri.getQueryParameter("callback");
        String callbackString = callbackUri.getHost();
        String dispatchContext = callbackUri.getQueryParameter("context");
        PlayHaven.d("Handling dispatch: %s of type %s", dispatchUrl, callbackString);

        switch (Dispatches.valueOf(callbackString)) {
            /**
             * closeButton hides the native emergency close button, and passes
             * notice of whether it was hidden back to the content template
             */
            case closeButton:
                String hidden = "true";
                try {
                    hidden = new JSONObject(dispatchContext).getString("hidden");
                } catch (JSONException jse) {
                    // Default to NOT hiding the emergency close button
                    hidden = "false";
                }

                if("true".equals(hidden)) {
                    ((PlayHavenView) getParent()).setExitVisible(false);
                }

                // Tell the content template that we've hidden the emergency close button.
                this.loadUrl(String.format(CALLBACK_TEMPLATE, callbackId, "{'hidden':'" + hidden + "'}", null));
                break;
            /**
             * dismiss triggers the contentDismissed listener
             */
            case dismiss:
                PlayHavenView.DismissType dismiss = PlayHavenView.DismissType.NoThanks;
                if(mRewards != null)
                    dismiss = PlayHavenView.DismissType.Reward;

                if(mDataFields != null)
                    dismiss = PlayHavenView.DismissType.OptIn;

                if(mPurchases != null)
                    dismiss = PlayHavenView.DismissType.Purchase;

                mPlacement.getListener().contentDismissed(mPlacement, dismiss, generateResponseBundle());
                
                // Unregister the web view client so that any future dispatches will be ignored. 
                HTMLView.this.setWebViewClient(null);
                
                break;
            /**
             * launch retrieves a URL from the server to be parsed using
             * Intent.ACTION_VIEW
             */
            case launch:
                mPlacement.getListener().contentDismissed(mPlacement, PlayHavenView.DismissType.Launch, null);

                /*
                 * We can't get this from the original model because we don't
                 * know which one they picked (if this was a more_games template).
                 */
                String url;
                try {
                    url = new JSONObject(dispatchContext).getString("url");
                } catch (JSONException jse) {
                    PlayHaven.e("Could not parse launch URL.");
                    return;
                }

                UrlRequest urlRequest = new UrlRequest(url);
                ExecutorService pool = Executors.newSingleThreadExecutor();
                final Future<String> uriFuture = pool.submit(urlRequest);
                final String initialUrl = url;
                
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        // Wait for our final link.
                        String url = null;
                        try {
                            url = uriFuture.get();
                        } catch (Exception e) {
                            PlayHaven.v("Could not retrieve launch URL from server. Using initial url.");

                            // If the redirect failed, proceed with the original url. 
                            url = initialUrl;
                        }

                        // Launch whatever it is. It might be a Play, web, or other link 
                        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK );
                        try {
                            HTMLView.this.getContext().startActivity(intent);
                        } catch (Exception e) {
                        	PlayHaven.e("Unable to launch URI from template.");
                        	e.printStackTrace();
                        }
                    }
                }).start();
                break;
            /**
             * loadContext passes the full "context" JSON blob to the 
             * content template
             */
            case loadContext:
                this.loadUrl(DISPATCH_PROTOCOL_TEMPLATE);
                net.minidev.json.JSONObject context = JsonUtil.getPath(mPlacement.getModel(), "$.response.context");
                this.loadUrl(String.format(CALLBACK_TEMPLATE, callbackId, context, null));
                break;
            /**
             * purchase stores the purchase object (which is generated by the
             * content template) as mPurchases, for use with dismiss dispatch
             */
            case purchase:
                collectAttachments(dispatchContext);
                break;
            /**
             * reward stores the reward object (which is generated by the
             * content template) as mRewards, for use with dismiss dispatch
             */
            case reward:
                net.minidev.json.JSONObject rewardParam = JsonUtil.getPath(mPlacement.getModel(), "$.response.context.content.open_dispatch.parameters");
                if(rewardParam == null || rewardParam.size() == 0) {
                    // data_collection template sends a reward dispatch when it submits form data ...
                    // @TODO: have templates return more than key/value pairs (eg class, pattern)
                    this.loadUrl(COLLECT_FORM_DATA);
                }

                collectAttachments(dispatchContext);
                break;
            /**
             * subcontent takes a JSON blob generated by the content template
             * and uses that to get data for a new impression, currently a
             * more_games widget that follows a featured ad
             */
            case subcontent:
				SubcontentRequest subcontentRequest = new SubcontentRequest(dispatchContext);
				subcontentRequest.send(getContext());
                break;
            /**  @TODO Find out why this dispatch was abandoned in 1.12 */
            case track:
                PlayHaven.d("track callback not implemented.");
                break;
            /**
             * This is one injected to let the Android SDK harvest data from the 
             * opt-in data collection form. 
             */
            case dcData:
                try {
                    mDataFields = DataCollectionField.fromUrl(callbackUri);
                } catch (PlayHavenException e) {
                    e.printStackTrace();
                }
                break;
            default:
                break;
        }
    }

    /**
     * Parses rewards and purchases out of the model and stores them for
     * disbursal upon dismiss dispatch
    */
    public void collectAttachments(String dispatchContext) {
        if(JsonUtil.hasPath(dispatchContext, "$.purchases"))
            mPurchases = Purchase.fromJson(dispatchContext);

        if(JsonUtil.hasPath(dispatchContext, "$.rewards"))
            mRewards = Reward.fromJson(dispatchContext);
    }

    /**
     * Loads a url into the this webview, ensures 
     * that load occurs on UI thread. 
     * @param url to load 
     */
    public void load(final String url) {
        this.post(new Runnable() {
            @SuppressLint("InlinedApi")
			@Override
            public void run() {
                String fileUrl = null;
                try {
                    Cache cache = new Cache(getContext());
                    File template = cache.getFile(new URL(url));
                    cache.close();
                    if (template != null && template.exists()) {
                        fileUrl = "file:///" + template.getAbsolutePath();
                    }
                } catch (Exception e) {
                    PlayHaven.e(e);
                }

                if (fileUrl != null && fileUrl.length() > 1) {
                    PlayHaven.v("Loading from cache: %s.", fileUrl);
                    HTMLView.this.loadUrl(fileUrl);
                } else {
                    HTMLView.this.loadUrl(url);
                }
                
                setBackgroundColor(0x00000000);
                // WebView has flickering & transparency issues 
                // when hardware acceleration is enabled. 
                if (Build.VERSION.SDK_INT >= 11){
                	setLayerType(View.LAYER_TYPE_SOFTWARE, null);
                }
            }
        });
    }

    @SuppressLint("SetJavaScriptEnabled")
    @Override
    public void setPlacement(Placement placement) {
        mPlacement = placement;
        this.getSettings().setJavaScriptEnabled(true);
        this.setWebViewClient(webViewClient);
        this.setWebChromeClient(webChromeClient);
        
        try {
        	// If loading new placement into existing view, start over. 
        	mImages = null;
        	// After API 11, we can intercept requests for images. 
        	if(Build.VERSION.SDK_INT >= 11) {
        		mImages = JsonUrlExtractor.getImages(mPlacement.getModel());
        	}
		} finally {
			if(mImages == null) mImages = new ArrayList<String>();
		}

        load(JsonUtil.<String>getPath(mPlacement.getModel(), "$.response.url"));
    }

    /**
     * Create a response bundle for passing back to the publisher
     *
     * @return bundle containing data
     */
    @Override
    public Bundle generateResponseBundle() {
        Bundle data = null;

        if(mRewards != null) {
            data = new Bundle();
            data.putParcelableArrayList(PlayHavenView.BUNDLE_DATA_REWARD, mRewards);
        }

        if(mDataFields != null) {
            if(data == null) data = new Bundle();
            data.putParcelableArrayList(PlayHavenView.BUNDLE_DATA_OPTIN, mDataFields);
        }

        if(mPurchases != null) {
            if(data == null) data = new Bundle();
            data.putParcelableArrayList(PlayHavenView.BUNDLE_DATA_PURCHASE, mPurchases);
        }

        return data;
    }

}